Skip to content

fix(cypher): frontend should always emit exclusive kind matchers BED-7495#37

Open
seanjSO wants to merge 2 commits intomainfrom
seanj/BED-7495
Open

fix(cypher): frontend should always emit exclusive kind matchers BED-7495#37
seanjSO wants to merge 2 commits intomainfrom
seanj/BED-7495

Conversation

@seanjSO
Copy link
Contributor

@seanjSO seanjSO commented Feb 24, 2026

Summary by CodeRabbit

  • New Features

    • Added an exclusive node-label matching mode for strict (non-overlapping) label checks.
  • Refactor

    • Propagated exclusive-matching option across query construction and translation for consistent behavior.
    • Migrated kind-ID mapping to instance-based mappings to remove shared global state and improve stability.

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds an IsExclusive boolean to KindMatcher, sets it when parsing node labels, carries it through constructors/copies, adjusts PostgreSQL kind-ID translation to pick array-contains vs overlap based on IsExclusive, and migrates kind-mapper state to instance-level maps and methods.

Changes

Cohort / File(s) Summary
KindMatcher Model
cypher/models/cypher/model.go
Added exported IsExclusive bool to KindMatcher, updated NewKindMatcher(reference, kinds, isExclusive) signature, propagated flag in copies, and grouped type declarations.
Frontend parsing
cypher/frontend/expression.go
Set IsExclusive = true for KindMatcher created from OC_NodeLabels (comment noting spec exclusivity).
PG translation
cypher/models/pgsql/translate/kind.go
Updated newPGKindIDMatcher(..., isExclusive bool) and translateKindMatcher to receive/propagate IsExclusive; choose OperatorPGArrayLHSContainsRHS when exclusive and IDs exist, otherwise use overlap/equals logic.
Query API callers
query/model.go
Updated callsites (KindIn, Kind, DeleteKind, DeleteKinds) to include IsExclusive: false (via NewKindMatcher(..., false)); minor local var refactor.
In-memory Kind mapper
drivers/pg/pgutil/kindmapper.go
Moved kind-ID state to instance (nextKindID, exported KindToID/IDToKind), added public mapping helpers (MapKindID, MapKindIDs, MapKind, MapKinds), and adjusted Put to use instance counter.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Frontend as Frontend Parser
  participant Model as KindMatcher Model
  participant Translator as PG Translator
  participant Postgres as PostgreSQL

  Client->>Frontend: submit query with node labels
  Frontend->>Model: NewKindMatcher(..., isExclusive=true)
  Model->>Translator: translateKindMatcher(kindMatcher{IsExclusive})
  Translator->>Translator: select operator based on IsExclusive (array-contains vs overlap)
  Translator->>Postgres: emit SQL using chosen operator
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • urangel
  • kpom-specter

Poem

🐇 I found a flag beneath a log,
IsExclusive tucked in label's name,
From parse to SQL I hopped along,
Operators changed — I wagged and came,
A tiny hop, a carrot-sized claim.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the primary change: ensuring the frontend emits exclusive kind matchers. The title is specific, concise, and directly relates to the main objective reflected in the changes across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch seanj/BED-7495

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@seanjSO seanjSO marked this pull request as ready for review February 24, 2026 21:24
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
query/model.go (1)

132-137: Kind() relies on zero-value for IsExclusive while sibling functions are explicit.

DeleteKind, DeleteKinds, and KindIn all explicitly set IsExclusive: false, but Kind() creates a KindMatcher via struct literal without it. The zero-value behavior is correct, but adding the explicit field (or using the constructor) would be consistent with the rest of this PR.

♻️ Consistency suggestion
 func Kind(reference graph.Criteria, kinds ...graph.Kind) *cypherModel.KindMatcher {
-	return &cypherModel.KindMatcher{
-		Reference: reference,
-		Kinds:     kinds,
-	}
+	return cypherModel.NewKindMatcher(reference, kinds, false)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@query/model.go` around lines 132 - 137, Kind() currently relies on the
zero-value for KindMatcher.IsExclusive while sibling functions (DeleteKind,
DeleteKinds, KindIn) set IsExclusive: false explicitly; update Kind() to set
IsExclusive: false in the returned cypherModel.KindMatcher struct literal (or
call the same constructor used by the siblings) so it matches the explicit
behavior of DeleteKind/DeleteKinds/KindIn and keeps consistency across the API.
cypher/models/pgsql/translate/kind.go (1)

12-34: isExclusive is silently ignored for edge/composite bindings.

For edges (line 26–30), the function always emits = ANY(...) regardless of isExclusive. This is likely correct since edges carry a single kind_id, but it's worth a brief comment at the edge branch to make this explicit — prevents future maintainers from wondering if it's an oversight.

📝 Suggested comment
 	case pgsql.EdgeComposite, pgsql.ExpansionEdge:
+		// Edges have a single kind_id, so the exclusive/overlap distinction does not apply.
 		treeTranslator.PushOperand(pgsql.CompoundIdentifier{binding.Identifier, pgsql.ColumnKindID})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypher/models/pgsql/translate/kind.go` around lines 12 - 34, The branch in
newPGKindIDMatcher that handles binding.DataType == pgsql.EdgeComposite or
pgsql.ExpansionEdge ignores the isExclusive flag (it always emits an = ANY(...)
expression); add a concise comment above the edge branch explaining that edges
store a single kind_id so exclusivity is not applicable and isExclusive is
intentionally ignored for EdgeComposite/ExpansionEdge, referencing
newPGKindIDMatcher, binding.DataType, and the edge branch to make intent
explicit for future maintainers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cypher/models/pgsql/translate/kind.go`:
- Around line 12-34: The branch in newPGKindIDMatcher that handles
binding.DataType == pgsql.EdgeComposite or pgsql.ExpansionEdge ignores the
isExclusive flag (it always emits an = ANY(...) expression); add a concise
comment above the edge branch explaining that edges store a single kind_id so
exclusivity is not applicable and isExclusive is intentionally ignored for
EdgeComposite/ExpansionEdge, referencing newPGKindIDMatcher, binding.DataType,
and the edge branch to make intent explicit for future maintainers.

In `@query/model.go`:
- Around line 132-137: Kind() currently relies on the zero-value for
KindMatcher.IsExclusive while sibling functions (DeleteKind, DeleteKinds,
KindIn) set IsExclusive: false explicitly; update Kind() to set IsExclusive:
false in the returned cypherModel.KindMatcher struct literal (or call the same
constructor used by the siblings) so it matches the explicit behavior of
DeleteKind/DeleteKinds/KindIn and keeps consistency across the API.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3be153 and ccf5a1a.

📒 Files selected for processing (4)
  • cypher/frontend/expression.go
  • cypher/models/cypher/model.go
  • cypher/models/pgsql/translate/kind.go
  • query/model.go

@seanjSO seanjSO force-pushed the seanj/BED-7495 branch 2 times, most recently from ad5ef1e to 7a63ae4 Compare February 24, 2026 22:38
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cypher/models/pgsql/translate/kind.go`:
- Around line 12-24: Add a guard in newPGKindIDMatcher to handle empty kindIDs
when isExclusive is true: if isExclusive && len(kindIDs) == 0, push a boolean
FALSE literal (e.g. pgsql.NewLiteral(false, pgsql.Boolean)) into treeTranslator
via treeTranslator.PushOperand and return as a completed expression so the
matcher yields no rows; otherwise keep the existing logic that uses
pgsql.OperatorPGArrayLHSContainsRHS and pgsql.OperatorPGArrayOverlap. This
ensures the @> path (OperatorPGArrayLHSContainsRHS) does not vacuously match all
rows when kindIDs is empty.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad5ef1e and 7a63ae4.

📒 Files selected for processing (4)
  • cypher/frontend/expression.go
  • cypher/models/cypher/model.go
  • cypher/models/pgsql/translate/kind.go
  • query/model.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • cypher/models/cypher/model.go
  • cypher/frontend/expression.go

@cweidenkeller
Copy link

approved with the stipulation of some scenario tests =)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
drivers/pg/pgutil/kindmapper.go (1)

89-95: ⚠️ Potential issue | 🟠 Major

Make Put idempotent for existing kinds.

Line 90 always allocates a fresh ID. If Put is called twice for the same kind, the mapper ends up with multiple IDs for one kind (stale reverse entries), which breaks one-to-one mapping guarantees.

🔧 Proposed fix
 func (s *InMemoryKindMapper) Put(kind graph.Kind) int16 {
+	if existingID, found := s.KindToID[kind]; found {
+		return existingID
+	}
+
 	kindID := s.nextKindID
 	s.nextKindID += 1
 
 	s.KindToID[kind] = kindID
 	s.IDToKind[kindID] = kind
 
 	return kindID
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drivers/pg/pgutil/kindmapper.go` around lines 89 - 95, The Put method on
InMemoryKindMapper currently always assigns a fresh ID (using nextKindID)
causing duplicate mappings; update Put to first check s.KindToID for an existing
entry and return that existing int16 if present, otherwise allocate a new ID,
set both s.KindToID[kind]=id and s.IDToKind[id]=kind and increment s.nextKindID,
ensuring no stale reverse entries are left and preserving one-to-one mapping
between KindToID and IDToKind.
🧹 Nitpick comments (1)
drivers/pg/pgutil/kindmapper.go (1)

12-13: Avoid exposing mutable mapping internals directly.

Lines 12-13 expose both maps publicly, so callers can mutate one side and break KindToID/IDToKind consistency. Prefer private fields with read-only accessors (or snapshot getters) to preserve invariants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drivers/pg/pgutil/kindmapper.go` around lines 12 - 13, Change the exported
map fields KindToID and IDToKind to unexported fields (e.g., kindToID, idToKind)
to prevent external mutation, update the constructor/factory that populates
them, and provide read-only accessors such as GetID(kind graph.Kind) (int16,
bool) and GetKind(id int16) (graph.Kind, bool); if callers need the whole
mapping, add snapshot methods like KindToIDSnapshot() map[graph.Kind]int16 and
IDToKindSnapshot() map[int16]graph.Kind that return copies to preserve internal
invariants and keep the two maps consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@drivers/pg/pgutil/kindmapper.go`:
- Around line 89-95: The Put method on InMemoryKindMapper currently always
assigns a fresh ID (using nextKindID) causing duplicate mappings; update Put to
first check s.KindToID for an existing entry and return that existing int16 if
present, otherwise allocate a new ID, set both s.KindToID[kind]=id and
s.IDToKind[id]=kind and increment s.nextKindID, ensuring no stale reverse
entries are left and preserving one-to-one mapping between KindToID and
IDToKind.

---

Nitpick comments:
In `@drivers/pg/pgutil/kindmapper.go`:
- Around line 12-13: Change the exported map fields KindToID and IDToKind to
unexported fields (e.g., kindToID, idToKind) to prevent external mutation,
update the constructor/factory that populates them, and provide read-only
accessors such as GetID(kind graph.Kind) (int16, bool) and GetKind(id int16)
(graph.Kind, bool); if callers need the whole mapping, add snapshot methods like
KindToIDSnapshot() map[graph.Kind]int16 and IDToKindSnapshot()
map[int16]graph.Kind that return copies to preserve internal invariants and keep
the two maps consistent.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7dadc28 and 559061e.

📒 Files selected for processing (1)
  • drivers/pg/pgutil/kindmapper.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants